package efg;

import interfacesFerrovia.DefsGlobais.TipoAtributo;

import java.util.ArrayList;
import java.util.List;

import efg.tasks.AccelerateTrainTask;
import efg.tasks.ChangeSwitchTask;
import efg.tasks.SchedulableTask;
import efg.tasks.BreakTrainTask;

/**
 * Classe que representa uma composicao.
 * 
 */
public class Train extends RailwayElement {

	/**
	 * Lista sequencial contendo os ids dos trechos que serao percorridos pela
	 * composicao
	 */
	private List<String> routeIds;

	/**
	 * Construtor que associa o objeto a um elemento na malha
	 * 
	 * @param id
	 *            Identificador do elemento na malha
	 * @param r
	 *            ferrovia ao qual a composicao pertence
	 */
	public Train(String id, Railway r) {
		super(r, id);

		routeIds = new ArrayList<String>();
		String[] vRoute = (String[]) getStaticAttribute(TipoAtributo.ITINERARIO);
		for (String track : vRoute) {
			routeIds.add(track);
		}

		assert (getId() != null);
	}

	/**
	 * Metodo que agenda a parada de uma composicao em sua estacao final
	 * 
	 * @param currentTime
	 *            Tempo atual para fins de agendamento
	 */
	public void scheduleStopTime(Integer currentTime) {
		assert (currentTime != null && currentTime >= 0);
		assert (getLength() != null);
		Track track = (Track) getCurrentPlace();
		assert (isLastTrack(track.getId()));

		Float breakSpace = getBreakSpace();

		Float distance;
		if (getCurrentDirection().equals(Direction.A_TO_B)) {
			distance = track.getLength() - getCurrentPosition();
		} else {
			distance = getCurrentPosition();
		}

		Integer time = currentTime
				+ new Float((distance - breakSpace) / getCurrentSpeed())
						.intValue();
		SchedulableTask task = new BreakTrainTask(time, this, 0f, false);
		registerTask(task);
	}

	/**
	 * Metodo que agenda a passagem de uma composicao pela proxima chave de seu
	 * itinerario
	 * 
	 * @param track
	 *            Tracho atual onde esta a composicao
	 * @param currentTime
	 *            Tempo atual para fins de agendamento
	 */
	public void checkSwitchSchedule(Track track, Integer currentTime) {
		assert (track != null && routeIds.contains(track.getId()));
		assert (routeIds.indexOf(track.getId()) <= routeIds.size() - 1);
		assert (currentTime >= 0);
		Direction direction;
		
		//verifica a direçao - descobrir
		if (getCurrentPlace() instanceof Switch) {
			Switch cuurSwitch = (Switch) getCurrentPlace();
			Switch aSwitch = track.getaSwitch();
			if (aSwitch != null && aSwitch.equals(cuurSwitch)) {
				direction = Direction.A_TO_B;
			} else {
				direction = Direction.B_TO_A;
			}
		} else {
			direction = this.getCurrentDirection();
		}

		assert (direction != null);

		String nextTrackId = routeIds.get(routeIds.indexOf(track.getId()) + 1);
		Track nextTrack = (Track) getElementById(nextTrackId);

		Float finalPosition = 0f;
		Switch nextSwitch = track.getaSwitch();
		if (direction.equals(Direction.A_TO_B)) {
			finalPosition = track.getLength();
			nextSwitch = track.getbSwitch();
		}

		//acelera a ate a velocidade maxima
		Float targetSpeed = this.getMaxSpeed();
		this.accelerateToMax(false);

		/**
		 * 
		 */
		for (;;) {
			assert (this.getTimeToReachPosition(finalPosition) > 0);
			Float switchArrivalTime = new Float(currentTime
					+ this.getTimeToReachPosition(finalPosition));
			Float passingTime = new Float(Math.ceil((this.getLength() + 2000)
					/ targetSpeed));

			SwitchPeriod conflicts = nextSwitch.getConflictPeriod(
					switchArrivalTime, switchArrivalTime
							+ passingTime.intValue());

			if (conflicts == null) {
				//nao ha mais conflitos - velocidade final = 
				
				//calcula o tempo que a chave devera comecar a mudar
				Float switchChangeTime = switchArrivalTime
						- (nextSwitch.getSwitchTime() + 20);
				
				//atualiza agenda da chave
				nextSwitch.allocTime(this, switchArrivalTime, switchArrivalTime
						+ passingTime.intValue());

				//cria uma task pra chave ser alterada no instante que a chave
				//comecar a mudar 
				SchedulableTask switchTask = new ChangeSwitchTask(
						switchChangeTime.intValue(), nextSwitch, track,
						nextTrack);
				registerTask(switchTask);
				
				// muda a velocidade do trem pra velocidade maxima assim que sair da chave	
				SchedulableTask accelerateTask = new AccelerateTrainTask(
						switchChangeTime.intValue()
								+ nextSwitch.getSwitchTime(), this, this
								.getMaxSpeed(), true);
				registerTask(accelerateTask);

				break;
			}
			//reduz a velocidade em 5%
			targetSpeed *= 0.95f;
			if (targetSpeed > this.getCurrentSpeed()) {
				this.accelerate(targetSpeed, false);
			} else if (targetSpeed < this.getCurrentSpeed()) {
				this.speedDown(targetSpeed, false);
			}
		}
	}

	/**
	 * Calculo do tempo de parada
	 * 
	 * @return Tempo necessario para a composicao parar com frenagem normal
	 */
	private Float getBreakSpace() {
		return getRailwayControl().consultaDistMinParada(getId());
	}

	/**
	 * Componente do trilho atual
	 * 
	 * @return Componente do trilho (Trecho/Chave) onde a composicao se encontra
	 */
	public RailwayElement getCurrentPlace() {
		String currentTrackId = (String) getAttribute(TipoAtributo.ID_TR_CH_ATUAL);
		assert (currentTrackId != null);
		return getElementById(currentTrackId);
	}

	/**
	 * Comprimento da composicao
	 * 
	 * @return Comprimento da composicao
	 */
	public Integer getLength() {
		return (Integer) getStaticAttribute(TipoAtributo.COMPRIMENTO);
	}

	/**
	 * 
	 * @return Velocidade maxima que a composicao pode trafegar
	 */
	public Float getMaxSpeed() {
		return (Float) getStaticAttribute(TipoAtributo.VELOCIDADE_MAX);
	}

	/**
	 * 
	 * @return Aceleracao maxima que a composicao pode fornecer em km/TIC
	 */
	public Float getMaxAccel() {
		return (Float) getStaticAttribute(TipoAtributo.ACELERACAO_MAX);
	}

	/**
	 * 
	 * @return Frenagem maxima da composicao em km/TIC
	 */
	public Float getMaxBreaking() {
		return (Float) getStaticAttribute(TipoAtributo.FRENAGEM_MAX);
	}

	public List<String> getRouteIds() {
		return routeIds;
	}

	public Integer getDepartureTime() {
		return (Integer) getStaticAttribute(TipoAtributo.PARTIDA);
	}

	public Float getCurrentPosition() {
		return (Float) getAttribute(TipoAtributo.POSICAO_ATUAL);
	}

	public Float getCurrentSpeed() {
		return (Float) getAttribute(TipoAtributo.VEL_ATUAL);
	}

	public Direction getCurrentDirection() {
		Boolean v = (Boolean) getAttribute(TipoAtributo.SENTIDO_ATUAL);
		assert (v != null);
		if (v) {
			return Direction.A_TO_B;
		} else {
			return Direction.B_TO_A;
		}
	}

	public Float getCurrentAccel() {
		Float accel = (Float) getAttribute(TipoAtributo.ACEL_ATUAL);
		assert (accel != null && accel >= 0f);
		return accel;
	}

	public Float getCurrentBreak() {
		Float breakk = (Float) getAttribute(TipoAtributo.FREN_ATUAL);
		assert (breakk != null && breakk >= 0f);
		return breakk;
	}

	public Boolean getIsMoving() {
		return (Boolean) getAttribute(TipoAtributo.EM_MOVIMENTO);
	}

	/**
	 * Calcula a posicao da composicao dentro do trecho atual em um determinado
	 * instante
	 * 
	 * @param time
	 *            Tempo a observar
	 * @return Posicao prevista
	 */
	public Float getPostionAtTime(Integer time) {
		assert (time != null && getRailwayControl() != null);
		return getRailwayControl().consultaPosicao(getId(), time);
	}

	/**
	 * Calcula o intervalo de tempo necessario para a composicao alcancar uma
	 * determinada posicao dentro do trecho atual
	 * 
	 * @param position
	 *            Posicao a observar
	 * @return Tempo previsto
	 */
	public Integer getTimeToReachPosition(Float position) {
		assert (position != null && getRailwayControl() != null);
		return getRailwayControl().consultaTempo(getId(), position);
	}

	/**
	 * Tenta acelerar uma composicao
	 * 
	 * @param speed
	 *            Velocidade alvo
	 * @param isUrgent
	 *            Indicativo de que eh urgente
	 */
	public void accelerate(Float speed, Boolean isUrgent) {
		assert (speed != null && isUrgent != null);
		getRailwayControl().acelerarComposicao(getId(), speed, isUrgent);
	}

	/**
	 * Tenta acelear a composicao a sua velocidade maxima
	 * 
	 * @param isUrgent
	 *            Indicativo de que eh urgente
	 */
	public void accelerateToMax(Boolean isUrgent) {
		assert (isUrgent != null);
		if (getCurrentSpeed() < getMaxSpeed()) {
			accelerate(getMaxSpeed(), isUrgent);
		}
	}

	/**
	 * Verifica se um determinado trcho é o ultimo do itinerario deste trem
	 * 
	 * @param track
	 *            Trecho a observar
	 * @return Indicativo de se eh o ultimo trecho
	 */
	public Boolean isLastTrack(Track track) {
		assert (track != null);
		return isLastTrack(track.getId());
	}

	/**
	 * Verifica se um determinado trcho é o ultimo do itinerario deste trem
	 * 
	 * @param track
	 *            Identificador do Trecho a observar
	 * @return Indicativo de se eh o ultimo trecho
	 */
	public Boolean isLastTrack(String trackId) {
		assert (trackId != null);
		assert (routeIds.contains(trackId));
		return routeIds.indexOf(trackId) == routeIds.size() - 1;
	}

	/**
	 * Tenta fream uma composicao
	 * 
	 * @param speed
	 *            Velovidade alvo
	 * @param isUrgent
	 *            Indicativo de urgencia
	 */
	public void speedDown(Float speed, Boolean isUrgent) {
		assert (speed != null && isUrgent != null);
		if (getCurrentSpeed() > speed) {
			getRailwayControl().frearComposicao(getId(), speed, isUrgent);
		}
	}

	/**
	 * Registra a possibilidade de colisao frontal da composicao em todos os
	 * trechos de seu itinerario
	 */
	public void createConfictList() {
		Direction direction = getCurrentDirection();
		Track currentTrack = (Track) getCurrentPlace();
		assert (direction != null);
		assert (routeIds.contains(currentTrack.getId()));
		Integer trackIndex = routeIds.indexOf(currentTrack.getId());
		while (trackIndex < routeIds.size()) {
			Track track = (Track) getElementById(routeIds.get(trackIndex));
			track.registerTrain(this, direction);

			if (trackIndex < routeIds.size() - 1) {
				Switch sswitch = track.getDestinySwitch(direction);
				Track nextTrack = (Track) getElementById(routeIds
						.get(trackIndex + 1));
				if (nextTrack.getaSwitch() != null
						&& nextTrack.getaSwitch().equals(sswitch)) {
					direction = Direction.A_TO_B;
				} else {
					direction = Direction.B_TO_A;
				}
			}
			trackIndex++;
		}
	}

	/**
	 * 
	 * @param speed
	 * @param urgent
	 */
	public void setSpeed(Float speed, Boolean urgent) {
		assert (speed != null && urgent != null);
		if (getCurrentSpeed() <= speed) {
			accelerate(speed, urgent);
		} else if (getCurrentSpeed() > speed) {
			speedDown(speed, urgent);
		}
	}

	/**
	 * Calcula proximo trecho do itinerario a partir de um trecho referencial
	 * @param currentTrack Trecho referencial
	 * @return Proximo trecho
	 */
	public Track getNextTrack(Track currentTrack) {
		assert (routeIds.contains(currentTrack.getId()));
		Integer idx = routeIds.indexOf(currentTrack.getId());
		if (idx < routeIds.size() - 1) {
			return (Track) getElementById(routeIds.get(idx + 1));
		}
		return null;
	}
}
